查看原文
其他

eBay云计算“网”事|网络丢包篇

谢文利 & 李程远 eBay技术荟 2022-03-15

供稿 | eBay IE Cloud Team

作者 | 谢文利 & 李程远

编辑 | 顾欣怡本文5994字,预计阅读时间18分钟更多干货请关注“eBay技术荟”公众号



导读

eBay云计算“网”事 | 网络超时篇中,我们针对主机网络中的延时突发问题进行了分析,可以看到,基于eBPF的代码级别工具带来的定位方法改变以及效率提升,非常有助于定位和解决问题。而eBay IE Cloud Team的目标,正是针对这些常规问题运用相似的工具来快速定位到症结所在。本期“网”事,我们将关注网络的另外一个常见问题——丢包。


01 

问题描述


eBay的某个大型应用在容器化到Kubernetes平台后,发现某个周期性数据下载业务会出现失败率相对之前较高的情况。如图1所示:

图1 业务下载失败环境对比(点击可查看大图)


02 

应用部属详情


该应用具有700多个Pod,形成如图2所示的3行240列的集群分布。第一行的Pod从外部下载大约40GB数据,存到本地磁盘卷上,该Pod同时会有其他进程读取数据,将数据发送给第二行相同列的Pod。第二行相同列的Pod通过socket接收数据,放到本地磁盘卷。有另外的进程,读取数据,将数据发送给第三行相同列的Pod。第三行的相同列的Pod会有socket接收数据,并存入本地卷。

数据的传输按块来进行,每个数据块大小是4M。当40G的数据在该列的所有机器上都传输完成时,则表示该列数据传输成功,否则失败。从多次测试下载任务的结果来看,出现问题的节点具有较强的随机性,并不会固定出错。

图2 3行240列的Pod业务分布形式

(点击可查看大图)为了高效维护Pod状态并且进行Pod之间的通信,每个Pod都会运行在基于Akka(https://akka.io/)的分布式集群上。集群中的每一个成员,都被集群中的某些成员监控,当其中任何一个监测点检测到这个被监测成员不可达时,这个不可达信息就会通过gossip协议传播到其他成员,集群中的其它成员都会标记这个成员不可达。监控节点是否可达,则是通过心跳机制来进行。监测点每秒发送一次心跳报文(request),被监测成员就会回复该心跳报文(reply)。在节点之间,会进行系统消息的传递,如果系统消息不能被送达一个节点,则会导致系统的不一致,这个节点会被设置为隔离(quarnteed)状态,并且它再不能从不可达状态中回来。

03 

问题分析


1. 心跳报文

从应用的日志来看,出现该问题的Pod,都具有在Akka的分布式架构被设置为隔离状态的情况,当节点被隔离后,数据传输也会停止。我们发现节点被设置为隔离状态的节点,都出现了心跳报文不能正常通信的情况,如图3所示。

发生端发送心跳的request报文后,并没有收到对端回复的reply报文。于是我们首先检测节点的心跳报文是否有什么地方出现了异常。

图3 发送心跳的请求报文后,没有收到心跳回复

(点击可查看大图)

通过分析tcpdump数据,得知heartbeat报文数据具有固定的特征,并且在某个固定的端口上运行,所以我们在每个节点上部署eBPF程序,统计发往容器eth0端口的heartbeat心跳数据,并且每隔10秒就打印统计数据情况。该eBPF通过容器的ip地址,tcp端口号以及数据包长度等特征,来判断是否是心跳报文,并计数统计。经过统计发现,从某个Pod发出去的heartbeat报文,和到达对端Pod eth0端口的heartbeat包数量符合,而在应用的日志里面,并没有收到heartbeat报文的日志打印。从netstat -s显示的结果,也没有看到明显的丢包情况。Akka架构本身,具有数据包的分发功能,在收到数据后,再将其分发给具体的处理线程,所以很容易就怀疑是Akka框架本身出现问题,并没有将数据正确传递传递给处理线程。于是和应用方讨论是否在Akka架构上有什么行为异常,我们则继续从节点系统层面进行其他的分析。
2. 问题复现

我们写了socket收发数据程序,模拟数据下载上传的行为,希望能够复现数据传输失败的现象。在几个节点上做测试,甚至将数据速率调整为业务发送数据量的两倍,也并没有相应的现象出现,复现失败。

3. pageout

在分析日志的时候,发现有些出错节点日志出现如图4显示时间不连续的情况,中间有几秒的时间并没有打出日志来。

图4 16:20:59.264 - 16:21:03.264 之间并没有日志输出(点击可查看大图)

因此在该时刻,应用行为可能出现异常,于是让应用人员排查该问题。而我们则查看在该时刻,系统是否出现了异常。从有些出错节点的metrics(图5)来看,在出现问题的时刻附近,会有系统每秒的pageout数量出现突发,超过1000k,也就是需要刷大于4G的数据到硬盘。

在Linux内存的配置上,有vm.dirty_ratio 和vm.dirty_background_ratio两个关键参数。参数vm.dirty_ratio指定了当脏页数量达到系统可用内存一定比例时,系统开始将脏页通过pdflush同步回写到磁盘。默认比例是20%;参数vm.dirty_background_ratio指定了当脏页数量达到系统可用内存一定比例的时候,将一定缓存的脏页异步回写到磁盘,默认比例是10%。前者是同步操作,会阻塞进程运行。当前节点可用的内存量在14G左右,节点上vm.dirty_ratio的配置是20%,因此在出现大量pageout的时候,需要flush的脏页数量会超过14G * 20% = 2.6G。应用如果具有新的IO操作,需要等待脏页刷到磁盘结束。而运行Pod的机器磁盘是HDD,刷新1G的脏页,需要数秒的时间。因此我们怀疑可能是这个导致了问题。

图5 pageOut/Sec存在burst

(点击可查看大图)

另外一个现象也将问题引到了该怀疑点。应用本身通过LD_PRELOAD加载了一个动态库libcache_flush.so,该动态库在write和read超过512MB的时候,会主动调用fdatasync()和posix_fadvise()来刷页。而具体为何额外增加了该行为,因为历史太过悠久,已经不可考。但是很怀疑当该应用运行在物理机的时候,是否也是因为发现了pageout过多,而添加了该逻辑。于是我们做了如下操作:
  1. 修改libcache_flush.so库,当read() 或者write()超过64M时,进行刷页,而不需要等到512M。提高刷页的频率,避免同时刷大量的脏页。
  2. 将vm.dirty_background_ratio从默认值10修改为1,该行为会导致更频繁刷新cache,避免脏页累积到vm.dirty_ratio的水平,而导致需要同步刷脏页,阻碍应用的执行。
在修改之后,从metrics来看,pageout/s不会有数据突变的现象,而是变得非常平稳,说明没有突发的大量数据脏页刷到磁盘。但是实际测试下来,数据下载失败的问题依然存在,并没有得到修复。
在这之间,我们还进行了其他方面的怀疑和优化,例如调整Akka配置参数等,都没有修复问题。因为这个是网络相关的问题,我们还是从网络着手,从细节再进行分析。
4. 网络拥塞

应用方同事在应用上增加日志,记录数据块的发送和接收情况。结果发现在heartbeat报文出现问题之前,Pod之间传递业务数据的时候,已经出现问题了,具体现象是:

  1. 发送方发送数据块N的时候,调用write() 接口,但是一直没有发送出去。
  2. 接收方在接收数据块N-2,调用read() 接口,但是一直没有读取到数据。

因为数据长期发送和接收异常,造成传输速率过低,发送方会判断当前的网络链路出现问题,并取消发送,造成任务失败。于是在所有的节点上部署daemonSet Pod, 通过ss命令来查看socket的相关信息,每秒输出ss结果。在节点出问题后,查看ss的输出历史记录,结果发现了一样的现象。在发送端,socket的发送队列淤积,数据发送不出去,而接收队列,一直没有接收到数据。

图6 发送端队列淤积,数据发送不出去

(点击可查看大图)

图7 接收端队列一直为空,接收不到数据(点击可查看大图)
从ss命令的输出结果里,发现了两个异常。
  • 输出时间不连续。本来设定为每秒都输出结果,但是出问题的时刻,时间从2019-12-04 12:57:12直接跳到了2019-12-04 12:57:20,程序的运行在此刻出了问题。该现象在某些节点上会出现,但并不是所有出问题的节点都有同样的现象。
  • skmem项带有的drop统计出现了突然增长,从841涨到了1533。这么短的时间,出现这么多包显然不正常,而且在很长的时间内都没有恢复。那么这些包都丢到了哪儿了呢?发送端有进行数据的重传,但是重传数据一直都被丢弃,导致发送端的窗口被占满而无法继续发送。

为了弄清楚数据包是怎么丢的,在通过ss命令获取socket信息的同时,也通过netstat -s来获取tcp上链接的统计信息。ss 可以得到每个链接的丢包信息,而netstat -s获取的则是该network namespace下所有链接的统计信息。但很奇怪的是,通过netstat -s获取到跟丢包相关的统计,都没有显示有如此大的突然丢包的情况发生。两边的统计对不上。
图8 netstat -s显示只有少量的丢包增长(点击可查看大图)
这个结果令我们感到很困惑,是否有什么数据包丢弃了,记录到socket的丢包信息里面,但是对netstat -s显示的SNMP MIB并没有做相关的统计呢?排查了下tcp协议栈的代码,确实有多处有这样的情况。从定位问题一开始,我们就认为tcp丢包不会造成业务下载失败的情况,丢包只会让发送接收数据的速率下降,tcp 协议栈会重传丢失的数据,从而保证业务下载数据恢复。从网络的排查来看,tcp数据包的一直丢弃导致了业务下载失败的发生,因此数据包因为什么而导致了丢弃,是排查问题的关键。
5. 数据丢包追踪

很幸运的是,此刻我们发现有方式可以重现该问题,将之前用于重现问题的小程序,部署到出现数据下载失败的机器上,有时候可以重现应用碰到的问题——发送端发送数据暂停,发送的socket队列里面有大量的数据存在,而接收端并没有接收到数据,接收的socket队列也为空。在数据接收端通过ss命令查看socket的丢包统计,也同样出现了瞬间的增长,而netstat -s显示的统计里面,同样没有出现很多的丢包情况。

通过模拟程序,我们能够在出现了问题的节点上重现之前的现象,这对于问题的定位提供了很大的帮助。于是在模拟问题的时候,同时在节点上运行bcc检查tcp丢包的tcpdrop工具,详见:

https://github.com/iovisor/bcc/blob/master/tools/tcpdrop.py

该工具基于eBPF,可以打印出TCP链接的丢包信息,并将丢包的时刻的函数调用栈进行打印。很疑惑的是,当通过ss命令显示socket链接出现大量丢包的时候,tcpdrop工具并没有打印出来相关的丢包信息。很显然,该工具并没有抓到所有的丢包信息。于是我们分析了下该bcc tcpdrop工具的实现:其通过kprobe 追踪了tcp_drop()的函数,在该函数调用的时候,记录下相关的链接信息,并且打印出调用栈。那为何该工具并没有抓取到所有的丢包信息呢?应该是有些地方丢包,并没有调用tcp_drop()函数,才会导致这样的情况。于是继续查看tcp的协议栈代码,发现在tcp_v4_rcv(),tcp_v4_do_rcv(), tcp_rcv_established()的tcp数据包处理函数中,都有直接调用__kfree_skb(), kfree_skb(),而不是调用tcp_drop()来对数据进行丢弃。在这些函数里面,也会有多种情况会导致调用kfree_skb()。结合netstat -s的结果,我们对丢包的地方进行排除:
  • tcp_v4_do_rcv()中,针对已经处理established状态的链接,不会调用kfree_skb(), 所以不会在这个地方丢包。
  • tcp_rcv_established()中,在skb->len = tcp_header_len(不带数据的ack包)的条件下,才会调用__kfree_skb(),所以不会在这个地方丢包。
所以tcp_v4_rcv()的嫌疑最大。如图9所示,tcp_v4_rcv()里面有多种情况会导致kfree_skb()被调用,例如ip头里面的ttl值异常,xfrm4_policy_check(),tcp_v4_inbound_md5_hash(),tcp_filter() 等对该数据skb检查出问题,都会导致数据包被丢弃,那么如何确定是哪一种情况引起的呢?

图9 多种情况会导致skb被丢弃和释放

(点击可查看大图)

排查了代码运行的实际情况,并且和相关的SNMP MIB信息相对照,列出几个重点的怀疑对象。基于eBPF的kprobe,对多个函数进行了probe,包括tcp_filter(), tcp_add_backlog()等,记录函数的调用和返回值。当skb属于指定的端口和ip时,会记录该函数的处理,最后统一输出该skb的函数处理情况。最终发现,之所以在socket的统计上看到大量丢包,是因为在tcp_v4_rcv()里,调用tcp_filter()函数返回为true,导致需要将skb丢弃。而在tcp_filter()里,会判断该skb->pfmemalloc是否为true,来决定是否将数据丢弃。如图10所示。

图10 判断skb->pfmemalloc值来决定是否丢

(点击可查看大图)

此处会增加socket上的PFMEMALLOCDROP MIB的统计值,说明该数据被丢弃的时候,还是会在netstat 显示的SNMP MIB中体现出来,而不在其他没有记录SNMP MIB统计的地方丢弃。但是netstat -s显示出来的PFMEMALLOCDROP统计是加1,而ss -i显示的丢包,为何是数十倍的增量呢?在tcp_filter()判断需要将包进行丢弃后,会调用sk_drops_add()函数来增加socket的丢包统计,而sk_drops_add()里,并不是简单地对丢包进行加1,而是加该skb对应的gso_segs个数, 也就是该skb包含多少个mss分片的个数。

图11 socket丢包统计

(点击可查看大图)

因为统计方式的不同,所以netstat -s显示的丢包统计和ss 显示的丢包信息不符合。在数据下载上传的过程中,数据包的长度会达到十几K的大小,因此ss显示的丢包个数,会是netstat -s显示出来统计值的数十倍。经过这个分析,数据丢包的原因也明确了。在出问题的时候,接收数据包的skb->pfmalloc=true, 导致了数据被tcp协议栈丢弃。那么我们需要搞懂pfmalloc是什么,为什么skb->pfmalloc 这个时候是true呢?对于这个问题,我们需要从Linux对内存的回收管理开始讲起。
6. 问题根因分析
Linux系统是以页为单位来分配和管理内存的,每个页的大小默认是4KB。为了提高对物理内存的访问速度,也可以将页分为2MB或者1GB的大页(Huge Page)来进行管理。
对于不同的硬件,其使用内存页的处理方式也不同,所以内核将属性相同的页划分到同一个 zone中。zone的划分和硬件相关,所以不同的处理器的架构可能不一样。通常在64位 x86处理上,典型的zone分配有DMA Zone、DMA32 Zone和Normal Zone,每个zone可以属于不同的NUMA节点。具体的zone信息可以从/proc/zoneinfo下查看。每个zone都有相应的内存页,zone与zone之间没有任何的物理分割,它只是Linux为了便于管理进行的一种逻辑意义上的划分。在设计上,位于低地址端的zone,会给高地址的zone预留一定比例的页。当地址的zone内存充足的时候,高地址zone可以向低地址zone申请预留的内存页。系统通过配置/proc/sys/vm/lowmem_reserve_ratio的参数来指定低地址zone对高地址zone的预留比例。内核的内存管理非常复杂,其中内存回收是一个重点内容。内存的回收包括匿名内存(Anonymous Pages)和页缓存(Page caches)的回收。匿名内存没有对应的后备文件,回收时需要借助swap机制将数据缓存到磁盘中,产生swap out,然后在需要时从磁盘导入内存页中。如果没有开启swap,可能会产生匿名内存无法回收的情况。对于页缓存的回收,如果不是被修改过的脏页,则可以直接从内存中回收;如果是脏页,则需要通过write back机制将数据写入磁盘后再进行回收。内存的使用状态直接决定了内存应该以何种方式被回收。在内核中,使用high、low、min 三条水线(Watermark)来衡量内存 zone的使用状态。

图12 内存zone的水线

(点击可查看大图)

如图12所示,内存的每个zone都具有high、low、min三条水线。zone的可用空闲内存页为其空余内存页减去给高端地址预留的部分。当可用的空闲内存页低于low水线但高于min水线时,说明内存的使用率比较高,需要进行内存回收。在内存页分配完成后,系统的kswapd进程将被唤醒,以执行内存回收操作。当可用空余内存回到high的水线后,内存回收就完成了。低地址端zone的可用空余内存页只有在高水线之上,才可以向高地址端的zone提供内存页。当可用的空闲内存低于min水线时,说明当前的内存使用量已经过载。如果应用程序申请分配内存,则会触发内存的直接回收操作,内存分配需要等待直接回收完成后才继续进行。很多系统进程,特别是像kswapd和内存回收直接相关的进程,在该情况下还要保证能够正常工作,因此需要带上PF_MALLOC的标记位,这样可以不受水线的限制而正常分配内存。默认情况下,系统总的min值通过/proc/sys/vm/min_free_kbytes来设置。该值和系统内总的内存值相关。根据系统总的min值,通过zone空间占有的比例来分配相应的min水线、low水线(默认是min水线的1.25倍)和high水线(默认是min水线的1.5 倍),用户可以通过/proc/zoneinfo来查看到详细的水线信息和可用页信息。从内存回收的行为来看,内存在配置的时候,要尽量避免空闲可用内存页到达直接回收的水线。在大规模申请内存的场景,内存的回收速度跟不上内存的分配速度,那么内存水线很可能会低于min水线。内核在给网络数据包分配skb的时候,发现当前的内存水线低于min水线,设置skb->pfmalloc=true。当tcp协议栈处理该类型的数据时,会根据socket类型,进行是否丢包并释放skb的操作。因此应用数据下载失败问题的发生,是因为系统在一段时间内一直处于内存紧缺状态,所以导致数据包一直被丢弃,最终应用停止发送数据,数据下载任务失败。

那么为什么Linux在给网络数据分配skb的时候,需要加上pfmalloc的标志位而不是直接分配不出来然后将数据丢弃呢?感兴趣的读者可以参考: https://lwn.net/Articles/439298/ 


04 

解决方案


在Linux上,我们通过 /proc/sys/vm/watermark_scale_factor 来配置不同内存水线之间的差值,让kswapd每次被触发回收内存时,可以一次回收更多的内存,尽量避免内存水线到达min水位。同时通过配置/proc/sys/vm/min_free_kbytes来抬高min水线,防止在极端情况下,系统没有内存可以使用,造成异常。

另外在节点上,可以通过部属eBPF程序来抓取系统的内存直接回收(direct_reclaim)事件,当有内存直接回收的事件发生时,说明内存的参数还需要继续优化。


05 

总结—网络丢包篇


常见的网络丢包问题,一般都可以通过网卡丢包统计,/proc/net/softnet,netstat统计等方式来查看丢包情况,也比较容易查看出丢包的原因所在。但是这些统计信息并不能反应出具体链接的具体丢包原因,而通过eBPF,则可以从代码级别提供一个很好的解决方法。

在tcpdrop工具的基础上,我们开发了另外一个追踪tcp丢包原因的工具。该工具记录了数据包在内核中被处理过的部分函数,如果产生了丢包,则会打印相关函数信息和函数堆栈。这样根据处理的函数信息,再结合代码,就可以推算出tcp数据包的丢包原因,最差的情况下也可以定位到丢包的函数级别。从定位过程可以看出,对于相对常规的丢包统计,eBPF能让我们更深入剖析内核,让常规的粗糙统计结果更精细化,在有大量链接的场景下,该特性尤其重要。下篇我们就来看看在有几万个链接的Software LoadBalancer上,如何利用eBPF来定位重传问题。

您可能还感兴趣:

eBay云计算“网”事 | 网络超时篇

数据之道 | 属性图在增强分析平台中的实践

数据之道 | SLA/SLE监控与告警

数据之道 | 进阶版Spark执行计划图

重磅 | eBay提出强大的轻量级推荐算法——洛伦兹因子分解机

实战 | 利用Delta Lake使Spark SQL支持跨表CRUD操作

一探究竟 | eBay流量管理之DSR在基础架构中的运用及优化

干货 | Rheos SQL: 高效易用的实时流式SQL处理平台


👇点击阅读原文,一键投递 

    eBay大量优质职位,等的就是你

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存